package proj3;

import java.util.Random;

import misc.Input_Handler;
import misc.not_Legal_Exception;
import evolution_superclasses.Genotype;
import evolution_superclasses.Phenotype;

public class Proj3_Phenotype extends Phenotype {
	private Neuron[] _neurons;
	private Double[][] _weights;
	private Input[] _inputs;
	private Neuron _motor_left;
	private Neuron _motor_right;
	private boolean _printInputs = false;
	
	// Should all the boxes move right each step?
	static boolean move_right = false;
	// Should the neuron network be modified to
	// disregard the recursive connections on
	// the hidden layer?
	static boolean modify_network = false;
	// Should the time constant variable range
	// be modified to represent values in [2,3]?
	static boolean modify_tc_range = false;
	 
	//log
	int[] _trackerX;
	int[] _boxX;
	int[] _boxY;
	int[] _boxWidth;
	int _area_width;
	int _area_height;
	int _player_width;
	int[][] _boxes; //totals, wins, losses

	public Proj3_Phenotype(Genotype gen) {
		super(gen);
	}
	
	public Proj3_Phenotype(Genotype gen, boolean b) {
		super(gen, b);
		try {
			develop(gen);
		} catch (not_Legal_Exception e) {
			e.printStackTrace();
		}
	}
	
	private boolean[] castShadows(int trackerX, int trackerWidth, int boxX, int boxWidth, int width) {
		boolean[] tracker = new boolean[trackerWidth];

		int endTracker = trackerX+trackerWidth-1;
		int endBox = boxX+boxWidth-1;
		int min = trackerX<boxX?trackerX:boxX;
		int max = endTracker>endBox?endTracker:endBox;
		
		for(int i = min; i <=max; i++){
			if(i <= width){
				if((trackerX <= i && endTracker >= i) && (boxX <= i && endBox >= i)) //Handles all cases where collision does not happen due to a wrap
					tracker[i-trackerX] = true;
			}else{
				int imod = i%width;
				while (imod < 0)imod += width;
				if(((trackerX <= imod && endTracker >= imod) || (trackerX < i && endTracker >= i)) 
					&& ((boxX <= imod && endBox >= imod) || (boxX < i && endBox >= i)))
				tracker[trackerX <= imod?imod-trackerX:i-trackerX] = true;
			}
		}
		return tracker;
	}
	public void set_printInputs(boolean b) {
		_printInputs = b;
	}

	public void setWeights(Double[][] weights) {
		_weights = weights;
	}
	
	public void setNeurons(Neuron[] neurons) {
		_neurons = neurons;
	}
	
	public void setMotorRight(Neuron neuron) {
		_motor_right = neuron;
	}
	
	public void setMotorLeft(Neuron neuron) {
		_motor_left = neuron;
	}
	
	public void setInputs(Input[] inputs) {
		_inputs = inputs;
	}
	
	public void updateNeurons(boolean[] collisions) {
		double tmp = 0;
		for (int i = 0; i < collisions.length; i++)
			 _inputs[i].set(collisions[i]);
		for (int i = 5; i < _neurons.length - 1; i++) {	// start with non-sensory and don't consider bias
			 double s = 0;
			 for (int j = 0; j < _neurons.length; j++) {
				 if (_weights[j][i] != null){
					 tmp = _neurons[j].getO()*_weights[j][i];
					 if(_printInputs){
						 System.out.println("Neuron "+j+" to "+i+": "+tmp);
					 }
					 s += tmp;
				 }
			 }
			 double t = _neurons[i].get_t();
			 double y = _neurons[i].get_y();
			 _neurons[i].set_y(1/t * (s-y));
			 if(_printInputs)
				 System.out.println(_neurons[i].get_y()+"\n"+_neurons[i].getO()+"\n");
		 }
	}
	
	private void calculate_fitness() {
		 boolean catchAll = Proj3_Loop.catchAll;
		 _boxes = new int[6][3];
		 Random rand = new Random();
		 int score = 0;
		 int num_boxes = 40;
		_player_width = _inputs.length;
		 int player_max_move = 4;
		 _area_width = 30;
		 _area_height = 15;
		 int box_min_width = 1;
		 int box_max_width = 6;
		 int big_boxes = 0;
		 
		 int numSensorHit = 0;
		 
		 //create log
		 int count = 0;
		 _trackerX = new int[num_boxes*_area_height];
		 _boxX = new int[num_boxes*_area_height];
		 _boxY = new int[num_boxes*_area_height];
		 _boxWidth = new int[num_boxes*_area_height];
	 
		 while (num_boxes --> 0) {
			 int box_width = box_min_width + rand.nextInt(box_max_width + 1 - box_min_width);
			 _boxes[box_width - 1][0] += 1;
			 int box_x = rand.nextInt(_area_width-1);
			 int box_y = _area_height;
			 int player_x = _area_width / 2 - _player_width / 2;
			 
			 if(box_width >= _player_width)big_boxes++;
			 
			 resetNeurons();
			 
			 while (box_y --> 0) {
				 //reset sensors
				 for (int i = 0; i < 5; i++)_inputs[i].set(false);
				 
				 // set sensors for overlaps
//				 boolean[] collisions = find_collisions(player_x, player_width, box_x, box_width, area_width);
				 boolean[] collisions = castShadows(player_x, _player_width, box_x, box_width, _area_width);
				 
				 updateNeurons(collisions);
				 
				 player_x += (int)(_motor_left.getO() * (-player_max_move - 1) + _motor_right.getO() * (player_max_move + 1)) % _area_width;
				 while (player_x < 0)player_x += _area_width;

				 _trackerX[count] = player_x;
				 _boxX[count] = box_x;
				 _boxY[count] = box_y;
				 _boxWidth[count++] = box_width;
				 
				 if (move_right) {
					 box_x += 1;
					 box_x = box_x % _area_width;
				 }
			 }

//			 boolean[] collides = find_collisions(player_x, player_width, box_x, box_width, area_width);
			 boolean[] collides = castShadows(player_x, _player_width, box_x, box_width, _area_width);
			 boolean hit = false;
			 
			 numSensorHit = 0;
			 for(boolean b : collides){
				 if(b){
					 numSensorHit++;
				 }
			 }
			 if(numSensorHit == _player_width || numSensorHit == box_width){
				 hit = true;
				 _boxes[box_width - 1][1] += 1;
				 score = catchAll?score+1:(box_width >= _player_width ? score-1 : score+1);
			 }else if(numSensorHit > 0 && box_width >= _player_width){//If catchall => no minus
				 score = catchAll?score:score-1;
			 }else{//Avoid
				 _boxes[box_width - 1][2] += 1;
			 }
		 }
		
		 this.set_fitness(catchAll?score:score+big_boxes);
	}
	
	private void resetNeurons() {
		for(Neuron n: _neurons){
			n.reset();
		}
	}

	public void visualize(){
		Input_Handler ih = new Input_Handler();
		try {
			System.out.println("Show graphics (y/n)");
			if(ih.readString().trim().toLowerCase().equals("y")){
				Visualizer v = new Visualizer( _area_width, _area_height, _trackerX, _player_width, _boxX, _boxY, _boxWidth );
				v.visualize();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	protected void develop(Genotype gen) throws not_Legal_Exception {
		// gen has 34*8 bits
		// gen.getLength();
		String genome = ((Proj3_Genotype) gen).get_binaryString();
		
		int l = 8; //byte length
		
		//populate inputs list with empty input objects
		_inputs = new Input[5];
		for (int i = 0; i < 5; i++) _inputs[i] = new Input();
		
		//weights: bytes 1-22
		double[] w = new double[22];
		for (int i = 0; i < w.length; i++) {
			w[i] = binaryToValue(genome.substring(i*l, i*l + l), -5.0, 5.0);
		}
		
		//biases: bytes 22-25
		double[] b = new double[4];
		for (int i = 0; i < b.length; i++) {
			b[i] = binaryToValue(genome.substring((22+i)*l, (22+i)*l + l), -10.0, 0.0);
		}
		
		//gains: bytes 26-29
		double[] gains = new double[4];
		for (int i = 0; i < gains.length; i++) {
			gains[i] = binaryToValue(genome.substring((26+i)*l, (26+i)*l + l), 1.0, 5.0);
		}
		
		//time constants: bytes 30-34
		double tc_min = 1.0;
		double tc_max = 2.0;
		if (modify_tc_range) {
			tc_min = 2.0;
			tc_max = 3.0;
		}
		double[] tcs = new double[4];
		for (int i = 0; i < tcs.length; i++) {
			tcs[i] = binaryToValue(genome.substring((30+i)*l, (30+i)*l + l), tc_min, tc_max);
		}
		
		_neurons = new Neuron[] {
				new SensorNeuron(new Input[]{_inputs[0]}),	// sensory input node 1
				new SensorNeuron(new Input[]{_inputs[1]}),	// sensory input node 2
				new SensorNeuron(new Input[]{_inputs[2]}),	// sensory input node 3
				new SensorNeuron(new Input[]{_inputs[3]}),	// sensory input node 4
				new SensorNeuron(new Input[]{_inputs[4]}),	// sensory input node 5
				new Neuron(0.0, gains[0], tcs[0]),				// hidden layer node 1
				new Neuron(0.0, gains[1], tcs[1]),				// hidden layer node 2
				new Neuron(0.0, gains[2], tcs[2]),				// motor control layer node 1
				new Neuron(0.0, gains[3], tcs[3]),				// motor control layer node 2
				new BiasNueron()										// bias node
		};
		
		_motor_left = _neurons[7];
		_motor_right = _neurons[8];
		
		_weights = new Double[][] {
			//[from]/[to]
				{ null, null, null, null, null, w[0], w[1], null, null, null },
				{ null, null, null, null, null, w[2], w[3], null, null, null },
				{ null, null, null, null, null, w[4], w[5], null, null, null },
				{ null, null, null, null, null, w[6], w[7], null, null, null },
				{ null, null, null, null, null, w[8], w[9], null, null, null },
				{ null, null, null, null, null,w[10],w[11],w[12],w[13], null },
				{ null, null, null, null, null,w[14],w[15],w[16],w[17], null },
				{ null, null, null, null, null, null, null,w[18],w[19], null },
				{ null, null, null, null, null, null, null,w[20],w[21], null },
				{ null, null, null, null, null, b[0], b[1], b[2], b[3], null },
		};
		
		
		if (modify_network) { //If we modify the network, eliminate self-recursion in hidden layer
			// Nodes 5 and 6 (0-indexed) make up the hidden layer.
			_weights[5][5] = null;
			_weights[6][6] = null;
		}
		
		calculate_fitness();
	}
	
	private static double binaryToValue(String binary, double min, double max) {
		double scaled = Integer.parseInt(binary, 2)/(Math.pow(2, binary.length()) - 1);
		return min + (max - min) * scaled;
	}

	@Override
	public String toString() {
		String s = "";
		s += "Fitness: " + this.get_fitness() + "\n";
		String boxSizesString = "";
		for (int i = 0; i < _boxes.length; i++) {
			boxSizesString += "\tSize " + (i + 1) + ": tot=" + _boxes[i][0] + " catch="+_boxes[i][1] + " avoid=" + _boxes[i][2] + "\n";
		}
		s += "Boxes:\n" + boxSizesString;
		return s;
	}
	
	public String printCTRNN() {
		String s = "Weights:\n";
		for(Double[] bs: _weights){
			for(Double b: bs){
				s+= b==null?"-\t":String.format("%.2f", b)+"\t";
			}
			s+= "\n";
		}
		
		for (int j = 0; j < _neurons.length; j++) {
			if(_neurons[j] instanceof SensorNeuron){
				s+= "\n"+j+"(sensor)\n";
			}else if(_neurons[j] instanceof BiasNueron){
				s+= "\n"+j+"(bias)\n";
			}else if(j < 7){
				s+= "\n"+j+"(hidden neuron)\n";
			}else{
				s+= "\n"+j+"(motor neuron)\n";
			}
			s+= "gain: "+(_neurons[j].get_g()==null?"-":String.format("%.4f", _neurons[j].get_g()))+
			"\ttime: "+(_neurons[j].get_t()==null?"-":String.format("%.4f", _neurons[j].get_t()))+
			"\ty: "+(_neurons[j].get_y()==null?"-":String.format("%.4f", _neurons[j].get_y()));
		}
		return s;
	}
	
	public String printOs(){
		String s = "Outputs:\n";
		
		for (int j = 0; j < _neurons.length; j++) {
			if(_neurons[j] instanceof SensorNeuron){
				continue;
			}else if(_neurons[j] instanceof BiasNueron){
				continue;
			}else if(j < 7){
				s+= "\n"+j+"(hidden neuron)\n";
			}else{
				s+= "\n"+j+"(motor neuron)\n";
			}
			s+= "O: "+(String.format("%.4f", _neurons[j].getO()))+"\n";
		}
		return s;
	}
	
	public String printAction(){
		
		int move = (int)(_motor_left.getO() * (-5) + _motor_right.getO() * (5)) % _area_width;
		return "Action: "+move;
	}

}
